package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;


@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    //构造函数注入 stringRedisTemplate
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //存储普通key，设置过期时间
    public void setWithExpireTime(String key, Object value, Long time, TimeUnit unit){
        String jsonStr = JSONUtil.toJsonStr(value);
        stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);
    }

    //存储热点key，设置逻辑过期时间
    public void setWithLogicExpireTime(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //在类LocalDataTime上添加时间只能通过调用方法
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        String jsonStr = JSONUtil.toJsonStr(redisData);
        //存储的热点key实际上不会过期
        stringRedisTemplate.opsForValue().set(key, jsonStr);
    }

    public <R, ID> R queryWithPassThrough
            (String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,
             Long time, TimeUnit unit) {
        //从redis中查询缓存
        String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //判断缓存是否命中（是否为空）
        if (StrUtil.isNotBlank(Json)) {
            //1、命中，返回信息（不为空）
            return JSONUtil.toBean(Json, type);
        }
        //通过判断键值为空或为null，以此来判断是否存在redis中
        //空字符串代表着上一次该请求发生了缓存穿透，接下来需要保护数据库不受该请求访问
        if (Json != null) {
            return null;
        }

        //2、未命中，根据id查询数据库
        R r = dbFallback.apply(id);
        //2.1、判断数据对象是否存在
        if (r == null) {
            //2.3不存在，返回错误信息
            //将空值写入redis
            setWithExpireTime(keyPrefix + id, null, CACHE_NULL_TTL, TimeUnit.SECONDS);
            return null;
        }
        //2.2存在，将信息写入到redis，返回信息
        setWithExpireTime(keyPrefix + id, r, time, unit);

        return r;
    }

    //互斥锁解决缓存击穿问题
    public <R, ID> R queryWithMutex
            (String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,
             Long time, TimeUnit unit) {
        //从redis中查询缓存
        String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //判断缓存是否命中（是否为空）
        if (StrUtil.isNotBlank(Json)) {
            //命中，返回信息（不为空）
            return JSONUtil.toBean(Json, type);
        }
        //通过判断键值为空或为null，以此来判断是否存在redis中
        //空字符串代表着上一次该请求发生了缓存穿透，接下来需要保护数据库不受该请求访问
        if (Json != null) {  //Json == ""
            return null;
        }

        //1实现缓存重建
        String lockKey = "LOCK_" + keyPrefix + id; //锁名
        R r;
        try {
            //1.1获取互斥锁
            Boolean lock = tryLock(lockKey);
            //1.2判断是否获取成功
            if (!lock) {
                //1.3获取失败，进入休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }

            //1.4获取成功，查询寻数据库
            //根据id查询数据库
            r = dbFallback.apply(id);
            //判断数据信息是否存在
            if (r == null) {
                //不存在，返回错误信息
                //将空值写入redis
                setWithExpireTime(keyPrefix + id, null, CACHE_NULL_TTL, TimeUnit.SECONDS);
                return null;
            }
            //存在，将信息写入到redis，返回信息
            setWithExpireTime(keyPrefix + id, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //1.5释放互斥锁，无论如何必须执行
            unlock(lockKey);
        }
        return r;
    }

    //建立缓存重建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTEOR
            = Executors.newFixedThreadPool(10);

    //针对热点key的缓存查询
    public <R,ID> R queryWithLogicalExpire
            (String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,
             Long time, TimeUnit unit) {
        //从redis中查询商铺缓存
        String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //判断缓存是否命中（是否为空）
        if (StrUtil.isBlank(Json)) {
            //1、未命中，返回空
            return null;
        }

        //2.命中，判断缓存是否过期
        //json反序列化为对象
        RedisData redisData = BeanUtil.toBean(Json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        //3.判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        //3.1未过期，返回商铺信息
        if (expireTime.isAfter(LocalDateTime.now())) {  //不能用 == 或 < >，这类时间的先后只能通过方法来比较
            return r;
        }
        //3.2过期，需要缓存重建
        //4.获取锁，通过商铺id获取仅属于该数据信息的锁
        String lockKey = "LOCK_" + keyPrefix + id;
        Boolean lock = tryLock(lockKey);
        // 判断是否获取成功
        if (lock) {
            //4.1获取成功，再次判断数据是否过期
            String newJson = stringRedisTemplate.opsForValue().get(keyPrefix+ id);
            RedisData redisData1 = JSONUtil.toBean(newJson, RedisData.class);
            if (redisData1.getExpireTime().isAfter(LocalDateTime.now())) {
                //未过期，说明在该线程之前已经有线程更新了数据
                unlock(lockKey);
                return JSONUtil.toBean((JSONObject) redisData1.getData(), type);
            }
            /*
              为什么需要双重检查？
              从多线程角度出发，如果该线程执行在 判断逻辑锁已经过期 到 获取锁 这一个过程中恰好已经有其他线程更改了逻辑时间并且释放了锁
              那么该线程依然能够获取到锁，只不过此时此刻逻辑时间已经被更新了，不需要再次更新
             */

            // 锁已经过期，说明该线程第一个接触数据库
            // 开辟新线程获取数据库信息，并返回旧数据
            CACHE_REBUILD_EXECUTEOR.submit(() -> {
                try {
                    //重建缓存
                    //1.查询数据库，获取数据
                    R r1 = dbFallback.apply(id);
                    //2.将数据写入redis
                    setWithLogicExpireTime(keyPrefix + id, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //4.2获取失败，返回旧数据
        return r;
    }


    //获取锁
    private Boolean tryLock(String key) {
        //setnx 执行成功返回 1
        Boolean frag = stringRedisTemplate.opsForValue().
                setIfAbsent(key, "1", LOCK_REDIS_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(frag);
    }
    //释放锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


}
